package com.gitee.wsl.doc.prosemirror.model.dom.parser

import com.fleeksoft.ksoup.Ksoup
import com.fleeksoft.ksoup.nodes.Node as DomNode
import com.fleeksoft.ksoup.nodes.Element
import com.gitee.wsl.doc.prosemirror.model.fragment.Fragment
import com.gitee.wsl.doc.prosemirror.model.mark.Mark
import com.gitee.wsl.doc.prosemirror.model.Schema
import com.gitee.wsl.doc.prosemirror.model.fragment.Slice
import com.gitee.wsl.doc.prosemirror.model.dom.parser.rule.ParseRule
import com.gitee.wsl.doc.prosemirror.model.dom.RegexPatterns
import com.gitee.wsl.doc.prosemirror.model.dom.parser.rule.StyleParseRule
import com.gitee.wsl.doc.prosemirror.model.dom.parser.rule.TagParseRule
import com.gitee.wsl.doc.prosemirror.model.dom.parser.rule.isStyleRule
import com.gitee.wsl.doc.prosemirror.model.dom.matches
import com.gitee.wsl.doc.prosemirror.model.dom.parser.rule.isTagRule
import com.gitee.wsl.doc.prosemirror.model.node.Node
import kotlin.collections.get

// A DOM parser represents a strategy for parsing DOM content into a
// ProseMirror document conforming to a given schema. Its behavior is
// defined by an array of [rules](#model.ParseRule).
class DOMParser(
    // The schema into which the parser parses.
    val schema: Schema,
    // The set of [parse rules](#model.ParseRule) that the parser
    // uses, in order of precedence.
    rules: List<ParseRule>
) {
    internal val tags = mutableListOf<TagParseRule>()
    internal val styles = mutableListOf<StyleParseRule>()
    internal val normalizeLists: Boolean
    internal val matchedStyles = mutableListOf<String>()

    // Create a parser that targets the given schema, using the given
    // parsing rules.
    init {
        rules.forEach { rule ->
            if (isTagRule(rule)) {
                this.tags.add(rule as TagParseRule)
            } else if (isStyleRule(rule)) {
                rule as StyleParseRule
                val prop = RegexPatterns.STYLE_PROP_REGEX.find(rule.style)?.groups?.firstOrNull()?.value
                if (prop != null && prop !in this.matchedStyles) {
                    this.matchedStyles.add(prop)
                }
                this.styles.add(rule)
            }
        }

        // Only normalize list elements when lists in the schema can't directly contain themselves
        this.normalizeLists = this.tags.firstOrNull { r ->
            val regex = "^(ul|ol)\\b".toRegex()
            if (!regex.containsMatchIn(r.tag) || r.node == null) {
                false
            } else {
                val node = schema.nodes[r.node]!!
                node.contentMatch.matchType(node) != null
            }
        } == null
    }

    @Suppress("UnusedPrivateMember")
    fun parseHtml(html: String, options: ParseOptions = ParseOptionsImpl()): Node {
        val derivedDOM = Ksoup.parse(html).body()
        return parse(derivedDOM, options)
    }

    // Parse a document from the content of a DOM node.
    fun parse(dom: DomNode, options: ParseOptions = ParseOptionsImpl()): Node {
        val context = ParseContext(this, options, false)
        context.addAll(dom, Mark.Companion.none, options.from, options.to)
        return context.finish() as Node
    }

    // Parses the content of the given DOM node, like
    // [`parse`](#model.DOMParser.parse), and takes the same set of
    // options. But unlike that method, which produces a whole node,
    // this one returns a slice that is open at the sides, meaning that
    // the schema constraints aren't applied to the start of nodes to
    // the left of the input and the end of nodes at the end.
    fun parseSlice(dom: DomNode, options: ParseOptions = ParseOptionsImpl()): Slice {
        val context = ParseContext(this, options, true)
        context.addAll(dom, Mark.Companion.none, options.from, options.to)
        return Slice.Companion.maxOpen(context.finish() as Fragment)
    }

    @Suppress("NestedBlockDepth", "ComplexCondition")
    internal fun matchTag(dom: DomNode, context: ParseContext, after: TagParseRule?): TagParseRule? {
        val start = if (after != null) this.tags.indexOf(after) + 1 else 0
        for (i in start until this.tags.size) {
            val rule = this.tags[i]
            if (matches(dom, rule.tag!!) &&
                (rule.namespace == null || (dom as? Element)?.tag()?.namespace() == rule.namespace) &&
                (rule.context == null || context.matchesContext(rule.context!!))
            ) {
                val getNodeAttrs = rule.getNodeAttrs
                if (getNodeAttrs != null) {
                    val result = getNodeAttrs(dom as Element)
                    if (!result.matches) continue
                    rule.attrs = result.attrs
                }
                return rule
            }
        }
        return null
    }

    @Suppress("ComplexCondition", "LoopWithTooManyJumpStatements")
    internal fun matchStyle(
        prop: String,
        value: String,
        context: ParseContext,
        after: StyleParseRule?
    ): StyleParseRule? {
        val start = if (after != null) {
            this.styles.indexOf(after) + 1
        } else {
            0
        }
        for (i in start until this.styles.size) {
            val rule = this.styles[i]
            val style = rule.style
            if (style.indexOf(prop) != 0 ||
                rule.context != null && !context.matchesContext(rule.context!!) ||
                // Test that the style string either precisely matches the prop,
                // or has an '=' sign after the prop, followed by the given
                // value.
                style.length > prop.length &&
                (style[prop.length] != '=' || style.slice(prop.length + 1 until style.length) != value)
            ) {
                continue
            }
            val getStyleAttrs = rule.getStyleAttrs
            if (getStyleAttrs != null) {
                val result = getStyleAttrs(value)
                if (!result.matches) continue
                rule.attrs = result.attrs
            }
            return rule
        }
        return null
    }

    companion object {

        @Suppress("MagicNumber", "ComplexMethod")
        internal fun schemaRules(schema: Schema): List<ParseRule> {
            val result = mutableListOf<ParseRule>()
            fun insert(rule: ParseRule) {
                val priority = rule.priority ?: 50
                var i = 0
                while (i < result.size) {
                    val next = result[i]
                    val nextPriority = next.priority ?: 50
                    if (nextPriority < priority) break
                    i++
                }
                result.add(i, rule)
            }

            schema.marks.forEach { (key, value) ->
                value.spec.parseDOM?.forEach { rule ->
                    val ruleToInsert = rule.copyRule()
                    insert(rule = ruleToInsert)
                    if (!(rule.mark != null || rule.ignore != null || (rule as? StyleParseRule)?.clearMark != null)) {
                        ruleToInsert.mark = key
                    }
                }
            }
            schema.nodes.forEach { (key, value) ->
                value.spec.parseDOM?.forEach { rule ->
                    val ruleToInsert = rule.copyRule() as TagParseRule
                    insert(ruleToInsert as ParseRule)
                    if (!((rule as? TagParseRule)?.node != null || rule.ignore != null || rule.mark != null)) {
                        ruleToInsert.node = key
                    }
                }
            }
            return result
        }

        // Construct a DOM parser using the parsing rules listed in a
        // schema's [node specs](#model.NodeSpec.parseDOM), reordered by
        // [priority](#model.ParseRule.priority).
        fun fromSchema(schema: Schema): DOMParser {
            return schema.cached.getOrPut("domParser") {
                DOMParser(schema, schemaRules(schema))
            } as DOMParser
        }
    }
}